<!DOCTYPE html>
<html>
  <head>
    <title>GoJS 自动网格布局</title>
    <script src="./gojs3.0.23.js"></script>
  </head>

  <body>
    <div
      id="myDiagram"
      style="width: 100%; height: 1000px; border: 1px solid #ccc"
    ></div>

    <script>
      go.licenseKey = "YOUR_LICENSE_KEY";
      const $ = go.GraphObject.make; // GoJS 简化语法

      const nodeIcons = {
        "1-1": "👨‍💼", // 运维人员图标
        "1-2": "💻", // 终端图标
        "1-3": "🖥️", // 服务器图标
        "1-4": "🖥️", // 服务器图标
        "1-5": "🖥️", // 服务器图标
        "1-6": "🖥️", // 服务器图标
        "1-7": "📦", // 数据库图标
        "2-1": "👨‍💼", // 项目经理图标
        "2-2": "💻", // 终端图标
        "2-3": "🖥️", // 服务器图标
        "3-1": "👨‍💼", // 运维人员图标
        "3-2": "💻", // 终端图标
      };

      // 创建 Diagram 实例
      const diagram = $(go.Diagram, "myDiagram", {
        initialContentAlignment: go.Spot.Center, // 内容居中
        "undoManager.isEnabled": true, // 启用撤销/重做
      });

      // 定义节点模板
      diagram.nodeTemplate = $(
        go.Node,
        "Vertical", // 使用垂直布局
        {
          selectionAdorned: true,
          resizable: false,
          layoutConditions: go.Part.LayoutStandard & ~go.Part.LayoutNodeSized,
        },
        // 图标部分
        $(
          go.Panel,
          "Auto",
          { margin: 0 }, // 为图标面板设置名称
          $(go.Shape, "Circle", {
            fill: "rgba(0,0,0,0)",
            stroke: null,
            width: 100,
            height: 80,
            alignment: go.Spot.Center,
            name: "SHAPE", // 为图标形状设置名称
          }),
          $(
            go.TextBlock,
            {
              font: "bold 50px Arial",
              margin: 0,
              alignment: go.Spot.Center,
              name: "ICON_TEXT", // 为图标文本设置名称
            },
            new go.Binding("text", "key", function (key) {
              return nodeIcons[key] || "❓"; // 默认问号图标
            })
          )
        ),
        $(
          go.TextBlock,
          {
            font: "12px Arial",
            margin: 0,
            textAlign: "center",
            wrap: go.TextBlock.WrapFit,
            maxLines: 10,
            name: "LABEL_TEXT", // 为标签文本设置名称
          },
          new go.Binding("text", (key) => {
            return key.text;
          })
        ),
        $(
          go.TextBlock,
          {
            font: "12px Arial",
            stroke: "#888",
            margin: 6,
            textAlign: "center",
            wrap: go.TextBlock.WrapFit,
            maxLines: 10,
            name: "LABEL_TEXT", // 为标签文本设置名称
          },
          new go.Binding("text", (key) => {
            return key.subText + "\n";
          })
        )
      );

      // 定义需要特殊路由的节点对
      const specialLinks = [
        { from: "1-2", to: "1-2" },
        { from: "1-4", to: "1-2" },
        { from: "1-5", to: "1-2" },
        { from: "1-1", to: "1-5" },
      ];

      // 自定义路由函数 - 实现向上的直角和多次转折
      function adjustLinkRoute(link) {
        if (!link || !link.fromNode || !link.toNode) return;

        const fromNode = link.fromNode;
        const toNode = link.toNode;

        const fromShape = diagram.findNodeForKey(link.data.from);
        const toShape = diagram.findNodeForKey(link.data.to);
        let fromShapeWidth = 100;
        let toShapeWidth = 100;

        // 获取连线的type属性
        const linkType = link.data.type || "default";

        // 根据连线类型设置颜色
        let linkColor = "#333"; // 默认颜色
        let lineWidth = 1; // 默认线宽

        if (linkType === "error") {
          linkColor = "#FF4136"; // 红色 - 错误
          lineWidth = 1; // 错误连线加粗
        } else if (linkType === "warning") {
          linkColor = "#FF851B"; // 橙色 - 警告
          lineWidth = 1; // 警告连线稍粗
        } else if (linkType === "default") {
          linkColor = "#333"; // 深灰色 - 默认
          lineWidth = 1;
        }

        // 如果无法获取形状对象，使用节点的实际尺寸
        if (fromShape && fromShape.actualBounds.isReal()) {
          fromShapeWidth = fromShape.actualBounds.width;
        }

        if (toShape && toShape.actualBounds.isReal()) {
          toShapeWidth = toShape.actualBounds.width;
        }

        // 计算节点宽度的一半
        const fromWidthHalf = fromShapeWidth / 2;
        const toWidthHalf = toShapeWidth / 2;

        // 检查是否是特殊连接
        const isSpecialLink = specialLinks.some(
          (special) =>
            special.from === fromNode.data.key && special.to === toNode.data.key
        );

        if (isSpecialLink) {
          // 特殊连接使用自定义路由
          link.routing = go.Link.AvoidsNodes;
          //link.curve = go.Link.Bezier;
          link.corner = 0;

          // 获取节点位置
          const fromPos = fromNode.position;
          const toPos = toNode.position;

          // 设置端点位置
          link.fromSpot = go.Spot.TopCenter;
          link.toSpot = go.Spot.TopCenter;

          // 计算连线的控制点以创建多次直角转折
          const controlPoints = new go.List();

          // 起点：节点位置X坐标 + 宽度的一半
          controlPoints.add(new go.Point(fromPos.x + fromWidthHalf, fromPos.y));

          // 第一个控制点：向上移动
          controlPoints.add(
            new go.Point(fromPos.x + fromWidthHalf, fromPos.y - 40)
          );

          // 第二个控制点：水平移动到目标节点上方
          controlPoints.add(
            new go.Point(toPos.x + toWidthHalf, fromPos.y - 40)
          );

          // 终点：目标节点位置X坐标 + 宽度的一半
          controlPoints.add(new go.Point(toPos.x + toWidthHalf, toPos.y));

          // 设置控制点
          link.points = controlPoints;

          // 为特殊连线设置不同的zIndex，防止重叠
          link.zOrder = 10;
        } else {
          // 普通连接使用避免节点路由
          link.routing = go.Link.AvoidsNodes;
          link.fromEndSegmentLength = 30;
          link.toEndSegmentLength = 20;
          link.corner = 0;
          link.zOrder = 1;

          // 计算节点位置和尺寸
          const fromNodePos = fromNode.position;
          const toNodePos = toNode.position;

          // 获取节点尺寸
          const fromNodeWidth = fromNode.actualBounds.width;
          const fromNodeHeight = fromNode.actualBounds.height;
          const toNodeWidth = toNode.actualBounds.width;
          const toNodeHeight = toNode.actualBounds.height;

          // 计算从节点右侧到目标节点左侧的连线
          if (fromNodePos.x < toNodePos.x) {
            // 从节点右侧连线，向上偏移10px
            link.fromSpot = new go.Spot(1, 0.5, 0, -20); // 右侧中间，向上偏移10px

            // 到节点左侧连线，向上偏移10px
            link.toSpot = new go.Spot(0, 0.5, 0, -20); // 左侧中间，向上偏移10px
          }
          // 计算从节点左侧到目标节点右侧的连线
          else if (fromNodePos.x > toNodePos.x) {
            // 从节点左侧连线，向上偏移10px
            link.fromSpot = new go.Spot(0, 0.5, 0, -20); // 左侧中间，向上偏移10px

            // 到节点右侧连线，向上偏移10px
            link.toSpot = new go.Spot(1, 0.5, 0, -20); // 右侧中间，向上偏移10px
          }
        }

        // 检查是否是自关联节点（连接到自身的节点）
        const isSelfLink = fromNode.data.key === toNode.data.key;
        if (isSelfLink) {
          link.fromSpot = new go.Spot(0, 0.5, 0, -20);
        }

        link.findObject("LINE").stroke = linkColor;
        link.findObject("LINE").strokeWidth = lineWidth;
        link.findObject("ARROW").stroke = linkColor;
        link.findObject("ARROW").fill = linkColor;
        link.findObject("LINK_LABEL").stroke = linkColor;

        // 重新计算路由
        link.updateRoute();
      }

      // 连线模板
      diagram.linkTemplate = $(
        go.Link,
        {
          relinkableFrom: true,
          relinkableTo: true,
          // 监听节点位置变化，使用定时器延迟触发路由调整，避免循环调用
          fromPortChanged: function (link) {
            // 使用定时器延迟执行，避免直接递归调用
            setTimeout(function () {
              adjustLinkRoute(link);
            }, 1);
          },
          toPortChanged: function (link) {
            // 使用定时器延迟执行，避免直接递归调用
            setTimeout(function () {
              adjustLinkRoute(link);
            }, 1);
          },
        },
        $(go.Shape, { strokeWidth: 1, name: "LINE" }),
        $(go.Shape, { toArrow: "OpenTriangle", name: "ARROW" }),
        $(
          go.TextBlock,
          {
            font: "10px Arial",
            segmentIndex: 0.5, // 标签位置：连线中点
            segmentOffset: new go.Point(0, -12), // 垂直偏移量
            segmentOrientation: go.Link.OrientUpright, // 保持文字正向
            name: "LINK_LABEL",
          },
          new go.Binding("text", "text", function (label) {
            return label || ""; // 如果没有标签文本，返回空字符串
          })
        )
      );

      // 配置网格布局
      diagram.layout = $(go.GridLayout, {
        wrappingColumn: 1, // 每行最多 3 个节点
        spacing: new go.Size(50, 30), // 同时设置水平和垂直间距
        isOngoing: true, // 数据变化时自动重新布局
        alignment: go.GridLayout.Position, // 位置对齐
        // 使用 comparer 替代 sorter
        comparer: function (a, b) {
          const groupA = a.data.group;
          const groupB = b.data.group;

          // 先按分组排序
          if (groupA < groupB) return -1;
          if (groupA > groupB) return 1;

          // 同组内按key排序
          return a.data.key.localeCompare(b.data.key);
        },
      });

      diagram.groupTemplate = $(
        go.Group,
        "Vertical",
        {
          background: "rgba(0,0,0,0)", // 分组背景色
          layout: $(go.GridLayout, {
            wrappingWidth: 9999999999,
            wrappingColumn: 9999999999, // 分组内不换行
            spacing: new go.Size(60, 60), // 节点间距
            alignment: go.GridLayout.Position, // 位置对齐
          }),
          padding: 10,
          selectionAdorned: false,
          selectable: false,
        },
        $(go.Placeholder, { padding: 10 })
      );

      // 示例数据（无 loc 字段）
      const nodeDataArray = [
        { key: "1", text: "", isGroup: true },
        { key: "2", text: "", isGroup: true },
        { key: "3", text: "", isGroup: true },
        {
          key: "1-1",
          group: "1",
          text: "张三(运维人员)",
          subText: "福建同明网络科技有限公司",
        },
        {
          key: "1-2",
          group: "1",
          text: "福建同明网络科技有限公司(运维终端)",
          subText: "192.168.1.1",
        },
        {
          key: "1-3",
          group: "1",
          text: "OA管理系统(应用服务器)",
          subText: "192.168.1.2",
        },
        {
          key: "1-4",
          group: "1",
          text: "互联网数据采集管理系统(应用服务器)",
          subText: "192.168.1.3",
        },
        {
          key: "1-5",
          group: "1",
          text: "上证指数智能分析下注管理系统(应用服务器)",
          subText: "192.168.1.4",
        },
        {
          key: "1-6",
          group: "1",
          text: "物流管理系统(应用服务器)",
          subText: "192.168.1.5",
        },
        {
          key: "1-7",
          group: "1",
          text: "物流管理系统MySQL数据库(数据库服务器)",
          subText: "192.168.1.6",
        },
        {
          key: "2-1",
          group: "2",
          text: "李四(项目经理)",
          subText: "福建超无敌网络科技有限公司",
        },
        {
          key: "2-2",
          group: "2",
          text: "福建超无敌网络科技有限公司(运维终端)",
          subText: "192.200.1.1",
        },
        {
          key: "2-3",
          group: "2",
          text: "传销情报分析系统(应用服务器)",
          subText: "192.200.1.2",
        },
        {
          key: "3-1",
          group: "3",
          text: "王五(运维人员)",
          subText: "福建超厉害网络科技有限公司",
        },
        {
          key: "3-2",
          group: "3",
          text: "福建超厉害网络科技有限公司(运维终端)",
          subText: "192.10.1.1",
        },
      ];

      // 添加连线数据
      const linkDataArray = [
        { from: "1-1", to: "1-2", type: "default", text: "" },
        { from: "1-2", to: "1-3", type: "default", text: "" },
        { from: "1-3", to: "1-4", type: "default", text: "" },
        { from: "1-4", to: "1-5", type: "default", text: "" },
        { from: "1-5", to: "1-6", type: "default", text: "" },
        { from: "1-6", to: "1-7", type: "default", text: "" },
        { from: "1-3", to: "2-3", type: "error", text: "违规访问\n绕行访问" },
        { from: "2-1", to: "2-2", type: "default", text: "" },
        { from: "2-2", to: "2-3", type: "default", text: "" },
        { from: "1-2", to: "2-3", type: "error", text: "违规访问\n绕行访问" },
        { from: "1-2", to: "1-4", type: "default", text: "" },
        { from: "1-4", to: "1-2", type: "default", text: "" },
        { from: "1-5", to: "1-2", type: "default", text: "" },
        { from: "1-1", to: "1-5", type: "default", text: "" },
        { from: "2-1", to: "2-3", type: "default", text: "" },
        { from: "3-2", to: "3-2", type: "warning", text: "自关联访问" },
        { from: "3-1", to: "3-2", type: "default", text: "" },
        { from: "3-2", to: "2-3", type: "default", text: "" },
        { from: "3-2", to: "2-2", type: "default", text: "" },
        { from: "2-3", to: "1-7", type: "default", text: "" },
      ];

      // 添加数据到 Diagram
      diagram.model = $(go.GraphLinksModel, {
        nodeDataArray: nodeDataArray,
        linkDataArray: linkDataArray, // 添加连线数据
      });

      // 图表加载完成后，调整所有连线
      diagram.addDiagramListener("InitialLayoutCompleted", function () {
        diagram.links.each(function (link) {
          adjustLinkRoute(link);
        });
      });

      // 监听图表变化，动态调整连线
      /*diagram.addModelChangedListener(function (e) {
        if (e.isTransactionFinished) {
          diagram.links.each(function (link) {
            adjustLinkRoute(link);
          });
        }
      });*/

      diagram.commandHandler.zoomToFit();
    </script>
  </body>
</html>
